package emby.lang.stdlib;

import java.io.FileNotFoundException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;

import emby.ASTNode;
import emby.EmbyRuntime;
import emby.Script;
import emby.err.InvalidArgumentError;
import emby.err.RestrictedAccessSyntaxError;
import emby.err.RuntimeError;
import emby.err.TypeError;
import emby.lang.EmbyArray;
import emby.lang.EmbyFunction;
import emby.lang.EmbyNull;
import emby.lang.JavaClassWrapper;
import emby.lang.NativeCastingFunctions;
import emby.lang.NativeFunction;
import emby.lang.Scope;
import emby.overload.OverloadType;
import emby.util.ReflectionHelp;
import embyparse.Parser;
import embyparse.TreeToCode;
import embyparse.tokenizer.StringTokenizer;

/**
 * DefaultFunctions contains functions that are automatically loaded into emby
 * when it starts up.
 * @author Justin
 */
public class DefaultFunctions {
	

	public static void addBuiltInFunctions(Scope global) {
		global.setVar("print", print);
		global.setVar("println", println);
		global.setVar("type", type);
		global.setVar("scope", scope);
		global.setVar("run", run);
		global.setVar("thread", thread);
		global.setVar("Array", Array);
		global.setVar("stringInput", strinput);
		global.setVar("eval", eval);
		global.setVar("nativefn", nativefn);
		global.setVar("jstatic", jstatic);
		global.setVar("jnew", jnew);
		global.setVar("jclass", jclass);	
		global.setVar("jimport", jimport);		
		global.setVar("$assign", cash_assign);
		global.setVar("exists", exists);
		global.setVar("remove",remove);
		global.setVar("quote", "\"");
		global.setVar("int", NativeCastingFunctions.Int);
		global.setVar("double", NativeCastingFunctions.Dbl);
	}


	public final static NativeFunction print = new NativeFunction("print") {
		private static final long serialVersionUID = 8572943857337772854L;
		public Object execute(int line, Scope s, Object... args) {
			if (args == null)
				return EmbyNull.Null;
			for (int i = 0; i < args.length; i++) {
				EmbyRuntime.out.print(args[i].toString());
			}
			return EmbyNull.Null;
		}
	};

	public final static NativeFunction println = new NativeFunction("println") {
		private static final long serialVersionUID = -8836692536276229645L;

		public Object execute(int line, Scope s, Object... args) {
			if (args == null) {
				EmbyRuntime.out.println();
				return EmbyNull.Null;
			}
			for (int i = 0; i < args.length; i++) {
				EmbyRuntime.out.print(args[i].toString());
			}
			EmbyRuntime.out.println();
			return EmbyNull.Null;
		}
	};

	/**
	 * Type returns a string representing the argument's data type.
	 * <br>
	 * Returns "int", "double", or "String" if the object is of a primitive type.
	 * Returns getClass.toString for java objects or objects that do not overload OverloadType.
	 * Else, returns the result of calling operatorType.
	 */
	public final static NativeFunction type = new NativeFunction("type") {
		private static final long serialVersionUID = 8572943857337772854L;
		public Object execute(int line, Scope s, Object... args) {
			if (argsMatch(line, args, Object.class)){
				if( args[0] instanceof OverloadType){
					return ((OverloadType)args[0]).operatorType(line, s);
				}else if (args[0] instanceof Integer){
					return "int";
				}else if (args[0] instanceof Double){
					return "double";
				}else if (args[0] instanceof String){
					return "String";
				}else{
					return args[0].getClass().getCanonicalName();
				}
			}else{
				return typeError(line, args);
			}
		}
	};

	/**
	 * fn scope( Scope superscope) This function returns a new scope extending
	 * superscope. If no superscope is specified, this will return a global
	 * scope.
	 */
	public final static NativeFunction scope = new NativeFunction("scope") {
		private static final long serialVersionUID = 8572943857337772854L;

		public Object execute(int line, Scope s, Object... args) {
			if (args.length == 0) {
				return new Scope();
			}else if (argsMatch(line,  args, EmbyNull.class)){
				return new Scope();
			}else if (argsMatch(line,  args, Scope.class)){
				Scope scp = (Scope) args[0];
				return new Scope(scp);
			}else{
				return typeError(line, args);
			}
		}
	};

	public final static NativeFunction run = new NativeFunction("run") {
		private static final long serialVersionUID = -8836692536276229645L;

		public Object execute(int line, Scope sc, Object... args) {
			if( argsMatch(line, args, String.class)){
				return execute((String) args[0], sc);
			}
			else if (argsMatch(line, args, String.class, Scope.class)){
				return execute((String) args[0], (Scope) args[1]);
			}else{
				return typeError(line, args);
			}
		}

		private Object execute(String filepath, Scope s) {
			Script<Object> script = null;
			try {
				script = new Script<Object>(filepath);
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			}
			return script.eval(s);
		}
	};

	public final static NativeFunction strinput = new NativeFunction(
			"strInput") {
		private static final long serialVersionUID = -8836692536276229645L;
		final java.util.Scanner s = new java.util.Scanner(System.in);

		public Object execute(int line, Scope sc, Object... args) {
			String cheese = s.nextLine();
			return cheese;
		}
	};

	public final static NativeFunction Array = new NativeFunction("ArrayGen"){
		private static final long serialVersionUID = 1145003443191238965L;

		public Object execute(int line, Scope s, Object... args) {
			if( argsMatch( line, args, Integer.class)){
				return new EmbyArray((Integer) args[0]);
			} else if (argsMatch(line, args, Integer.class, Object.class)){
				return new EmbyArray((Integer) args[0], args[1]);
			} else{
				return typeError(line, args);
			}
		}
	};

	public final static NativeFunction thread = new NativeFunction("thread") {
		private static final long serialVersionUID = 1145003443191238965L;

		public Object execute(final int line, final Scope s, Object... args) {
			final Object o = args[0];
			final Object[] fnArgs = Arrays.copyOfRange(args, 1, args.length);
			if (o instanceof EmbyFunction) {
				Runnable r = new Runnable() {
					public void run() {
						((EmbyFunction) o).execute(line, s, fnArgs);
					}
				};
				Thread t = new Thread(r, ((EmbyFunction) o).name);
				t.start();
			} else {
				throw new TypeError(line,
						"1st argument to thread must be a function");
			}
			return EmbyNull.Null;
		}
	};

	public final static NativeFunction eval = new NativeFunction("eval") {
		private static final long serialVersionUID = 3956452568225664793L;

		public Object execute(int line, Scope s, Object... args) {
			Scope scope = null;
			if( argsMatch( line, args, String.class)){
				scope = s;
			}else if (argsMatch( line, args, String.class, Scope.class)){
				scope = (Scope) args[1];
			}else{
				return typeError(line, args);
			}
			StringTokenizer t = new StringTokenizer(line, (String) args[0]);
			Parser p = new Parser(t);
			p.parse();
			ASTNode[] parsed = TreeToCode.treeToNodes(p.getParseStack());
			if (parsed.length != 1) {
				throw new RuntimeError(line,
						"eval can only run one expression or statement");
			}
			return parsed[0].eval(scope);
		}
	};

	/**
	 * nativefn is used to import functions defined in java to an emby environment.
	 * Functions defined in java must extend class emby.lang.NativeFunction
	 */
	@Deprecated
	public final static NativeFunction nativefn = new NativeFunction(
			"nativefn") {
		private static final long serialVersionUID = 8936260082233910206L;

		public Object execute(int line, Scope s, Object... args) {
			if( !argsMatch(line, args, String.class)){
				return typeError(line, args);
			}
			String className = (String) args[0];
			Class<NativeFunction> c = null;
			try {
				c = (Class<NativeFunction>) Class.forName(className);
			} catch (ClassNotFoundException e) {
				throw new RuntimeError(line, "nativefn could not find class "
						+ className);
			} catch (ClassCastException e1) {
				throw new RuntimeError(line, className
						+ " is not an EmbyFunction");
			}

			Constructor<NativeFunction> con = null;
			try {
				con = (Constructor<NativeFunction>) c.getDeclaredConstructors()[0];
			} catch (SecurityException e) {
				e.printStackTrace();
			}

			NativeFunction f = null;
			try {
				con.setAccessible(true);
				f = con.newInstance(className);
			} catch (IllegalArgumentException e) {
				throw new RuntimeError(
						line,
						className
								+ " was defined improperly. It must have only ONE constructor of signature (String)");
			} catch (InstantiationException e) {
				throw new RuntimeError(line,className+ " is not instantiable (abstract class, interface, array class, primitive type, or void )");
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
			return f;
		}
	};

	/**
	 * jstatic returns either the value of a field or a method wrapper.
	 */
	@Deprecated
	public final static NativeFunction jstatic = new NativeFunction(
			"jstatic") {
				private static final long serialVersionUID = 1148538343898070895L;
				@Override
				public Object execute(int line, Scope s, Object... args) {
					if( !(argsMatch(line, args, String.class, String.class))){
						typeError(line, args);
					}
					Class<? extends Object> c = null;
					try {
						c = Class.forName((String) args[0]);
					} catch (ClassNotFoundException e) {
						try { //Try ONE more time, appending java.lang in front of the class name.
							c = Class.forName("java.lang."+((String) args[0]));
						} catch (ClassNotFoundException e1) {
							throw new RuntimeError(line, "jstatic: Class "+args[0]+" not found.");
						}
					}
					return ReflectionHelp.getFieldValueOrMethodWrapper(line, c, (String)args[1], null);
				}
	};

	/**
	 * Jnew returns a constructor wrapper.
	 */
	@Deprecated
	public final static NativeFunction jnew = new NativeFunction(
			"jnew") {
				private static final long serialVersionUID = 1148538343898070895L;
				@Override
				public Object execute(int line, Scope s, Object... args) {
					if( args.length<1){
						return new InvalidArgumentError(line,"Not enough arguments. Usage: jnew(classname, constructoArgs)");
					}
					if( !(args[0] instanceof String)){
						return new TypeError(line,"Class name must be a string");
					}
					Class<? extends Object> c = null;
					try {
						c = Class.forName((String) args[0]);
					} catch (ClassNotFoundException e) {
						try { //Try ONE more time, appending java.lang in front of the class name.
							c = Class.forName("java.lang."+((String) args[0]));
						} catch (ClassNotFoundException e1) {
							throw new RuntimeError(line, "jstatic: Class "+args[0]+" not found.");
						}
					}
					return ReflectionHelp.getConstructorWrapper(line, c);
				}
	};
	
	/**
	 * Returns a class wrapper which either acts as a constructor when called or
	 * returns wrappers to fields/methods when accessed with dot notation.
	 */
	public final static NativeFunction jclass = new NativeFunction(
			"jclass") {
				private static final long serialVersionUID = 1148538343898070895L;
				@Override
				public Object execute(int line, Scope s, Object... args) {
					if( args.length!=1){
						return new InvalidArgumentError(line,"Not enough arguments. Usage: jclass(classname)");
					}
					if( !(args[0] instanceof String)){
						return new TypeError(line,"Class name must be a string");
					}
					return new JavaClassWrapper(line, (String)args[0], globalImports);
				}
	};
	
	public static final ArrayList<String> globalImports = JavaClassWrapper.getDefaultImports();
	/**
	 * Add an import to the jclass function.
	 */
	public final static NativeFunction jimport = new NativeFunction(
			"jimport") {
				private static final long serialVersionUID = 1148538343898070895L;
				@Override
				public Object execute(int line, Scope s, Object... args) {
					if( args.length!=1){
						return new InvalidArgumentError(line,"Not enough arguments. Usage: jimport(packagename)");
					}
					if( !(args[0] instanceof String)){
						return new TypeError(line,"Package name must be a string");
					}
					globalImports.add(JavaClassWrapper.formatImport((String)args[0]));
					return EmbyNull.Null;
				}
	};
	
	/**
	 * $assign assigns variables to a new value.
	 */
	public final static NativeFunction cash_assign = new NativeFunction(
			"$assign") {
				private static final long serialVersionUID = 1148538343898070895L;
				@Override
				public Object execute(int line, Scope s, Object... args) {
					Scope targetScope = s;
					if( (argsMatch(line, args, String.class, Object.class))){
						
					}else if ((argsMatch(line, args, String.class, Object.class, Scope.class))) {
						targetScope = (Scope)args[2];
					}else {
						typeError(line, args);
					}
					String varname = (String) args[0];
					Object varval = args[1];
					if( varname.startsWith("$") && targetScope.contains(varname)){
						throw new RestrictedAccessSyntaxError(line, varname);
					}else {
						targetScope.setVar(varname, varval);
					}
					return EmbyNull.Null;
				}
	};

	
	/**
	 * exists checks if a variable exists 
	 */
	public final static NativeFunction exists = new NativeFunction(
			"exists") {
				private static final long serialVersionUID = 1148538343898070895L;
				@Override
				public Object execute(int line, Scope s, Object... args) {
					Scope targetScope = s;
					if( (argsMatch(line, args, String.class))){
						
					}else if ((argsMatch(line, args, String.class, Scope.class))) {
						targetScope = (Scope)args[1];
					}else {
						typeError(line, args);
					}
					String varname = (String) args[0];
					return targetScope.containsSuper(varname);
				}
	};
	
	/**
	 * remove removes a variable from a scope 
	 */
	public final static NativeFunction remove = new NativeFunction(
			"remove") {
				private static final long serialVersionUID = 1148538343898070895L;
				@Override
				public Object execute(int line, Scope s, Object... args) {
					Scope targetScope = s;
					if( (argsMatch(line, args, String.class))){
						
					}else if ((argsMatch(line, args, String.class, Scope.class))) {
						targetScope = (Scope)args[1];
					}else {
						typeError(line, args);
					}
					String varname = (String) args[0];
					if( varname.startsWith("$") && targetScope.contains(varname)){
						throw new RestrictedAccessSyntaxError(line, varname);
					}
					return targetScope.remove(varname);
				}
	};
}
